/******************************************************************************* * Signavio Core Components * Copyright (C) 2012 Signavio GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package org.oryxeditor.server.diagram.generic; import java.awt.Color; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.log4j.Logger; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.oryxeditor.server.diagram.Bounds; import org.oryxeditor.server.diagram.Point; import org.oryxeditor.server.diagram.StencilSetReference; import org.oryxeditor.server.diagram.label.Anchors; import org.oryxeditor.server.diagram.label.Anchors.Anchor; import org.oryxeditor.server.diagram.label.EdgePosition; import org.oryxeditor.server.diagram.label.HorizontalAlign; import org.oryxeditor.server.diagram.label.LabelOrientation; import org.oryxeditor.server.diagram.label.LabelSettings; import org.oryxeditor.server.diagram.label.LabelStyle; import org.oryxeditor.server.diagram.label.VerticalAlign; /** * Parses a BasicDiagram out of a given JSON string or object. * <p/> * Subclasses <b>MUST</b> override {@link #createNewDiagram(String)}, {@link #createNewEdge(String)} and {@link #createNewNode(String)} according to the type of shapes they need! * @author Philipp Maschke * * @param <S> the actual type of shape to be used (must inherit from {@link GenericShape}); calls to {@link GenericShape#getChildShapesReadOnly()}, ... will return this type * @param <D> the actual type of diagram to be used (must inherit from {@link GenericDiagram}); {@link GenericShape#getDiagram()} will return this type */ public abstract class GenericDiagramBuilder <S extends GenericShape<S,D>, D extends GenericDiagram<S,D>, E extends GenericEdge<S, D>, N extends GenericNode<S, D>> implements ShapeFactory<S, D, E, N>{ private static final Logger LOGGER = Logger.getLogger(GenericDiagramBuilder.class); protected String parseStencilsetNamespaceInternal(JSONObject json) throws JSONException{ if (json != null && json.has("stencilset")) { JSONObject jsonStencilset = json.getJSONObject("stencilset"); if (jsonStencilset.has("namespace")) return jsonStencilset.getString("namespace").trim(); } return null; } protected String parseDiagramIdInternal(JSONObject json) throws JSONException, IllegalArgumentException { if (json == null) throw new IllegalArgumentException("JSON object is null"); String id = "canvas"; if (json.has("resourceId")) { id = json.getString("resourceId"); } return id; } /** * Parses the json object to the diagram model, assumes that the json is hierarchical ordered * * @param json * hierarchical JSON object representing a diagram * @return a diagram object with all shapes as defined in json * @throws JSONException * if JSON could not be parsed correctly * @throws IllegalArgumentException * if json is null */ public D parse(JSONObject json) throws JSONException { if (json == null) throw new IllegalArgumentException("JSON object is null"); Map<String, S> shapesMap = new HashMap<String, S>(); Map<String, String> targetsMap = new HashMap<String, String>(); Map<String, List<String>> outgoingsMap = new HashMap<String, List<String>>(); Map<String, List<String>> childsMap = new HashMap<String, List<String>>(); Map<String, JSONObject> flatJSON = flatRessources(json); // diagram specific parsing String id = "canvas"; if (json.has("resourceId")) { id = json.getString("resourceId"); } flatJSON.remove(id); D diagram = createNewDiagram(id); S diagramShape = (S) diagram;//only to reduce the number of warnings parseStencilSet(json, diagram); parseSsextensions(json, diagram); parseStencil(json, diagramShape);//, diagram.getStencilsetRef()); parseProperties(json, diagramShape); parseChildShapes(json, diagramShape, childsMap); parseBounds(json, diagramShape); shapesMap.put(diagram.getResourceId(), diagramShape); //parse all shapes and add them to the map for (Map.Entry<String, JSONObject> entry : flatJSON.entrySet()) { shapesMap.put(entry.getKey(), parseShape(entry.getKey(), entry.getValue(), diagram, targetsMap, outgoingsMap, childsMap)); } // setting these has been deferred so far fillInOutgoings(shapesMap, outgoingsMap); fillInTargets(shapesMap, targetsMap); fillInChildren(shapesMap, childsMap); // fillInIncomings(shapesMap.values()); fillSources(shapesMap.values()); return diagram; } /** * Set the child relation based on childMap's entries. * * @param shapesMap * @param childMap */ protected void fillInChildren(Map<String,S> shapesMap, Map<String, List<String>> childMap) { // iterate through the existing shape IDs, // iterate through its child shapes' IDs, and // add the looked-up children to the looked-up shape for (Entry<String, List<String>> childsEntry: childMap.entrySet()) { S shape = shapesMap.get(childsEntry.getKey()); for (String childId : childsEntry.getValue()) { S childShape = shapesMap.get(childId); shape.addChildShape(childShape); } } } /** * Set the target relation based on targetMap's entries. * * @param shapesMap * @param targetMap */ protected void fillInTargets(Map<String,S> shapesMap, Map<String, String> targetMap) { // iterate through the existing shape - target mappings, // set the looked-up target to the looked-up shape for (Entry<String, String> targetEntry : targetMap.entrySet()) { S shape = shapesMap.get(targetEntry.getKey()); if (shape != null && shape instanceof GenericEdge){ S targetShape = shapesMap.get(targetEntry.getValue()); if (targetShape != null) ((GenericEdge<S, D>)shape).connectToATarget(targetShape); } } } /** * Set the outgoing relation based on outgoingMap's entries. * * @param shapesMap * @param outgoingMap */ protected void fillInOutgoings(Map<String,S> shapesMap, Map<String, List<String>> outgoingMap) { // iterate through the existing shape IDs, // iterate through its child shapes' IDs, and // add the looked-up children to the looked-up shape for (Entry<String, List<String>> outgoingsEntry : outgoingMap.entrySet()) { S shape = shapesMap.get(outgoingsEntry.getKey()); for (String outgoingId : outgoingsEntry.getValue()) { S outgoingShape = shapesMap.get(outgoingId); shape.addOutgoingAndUpdateItsIncomings(outgoingShape); } } } protected void fillSources(Collection<S> shapes) { for (S s : shapes) { if (s instanceof GenericEdge) { S source = determineSource((GenericEdge<S, D>) s); if (source != null) ((GenericEdge<S, D>) s).connectToASource(source); } } } /** * A source of an edge is the one incoming shape, that doesn't have that shape as target * * @param s * @return */ protected S determineSource(GenericEdge<S, D> s) { if (s.getIncomingsReadOnly() == null || s.getIncomingsReadOnly().size() == 0) return null; else if (s.getIncomingsReadOnly().size() == 1) { S incoming = s.getIncomingsReadOnly().get(0); if (incoming instanceof GenericEdge && ((GenericEdge<S,D>) incoming).getTarget() != null && ((GenericEdge<S,D>) incoming).getTarget().equals(s)) return null; else return incoming; } else {// if more than one incomings return the one that doesn't have // 'this' as target (there should only be one!) List<S> incomings = new ArrayList<S>(s.getIncomingsReadOnly()); Iterator<S> it = incomings.iterator(); while (it.hasNext()) { S incoming = it.next(); if (incoming instanceof GenericEdge) { S target = ((GenericEdge<S,D>) incoming).getTarget(); if (target != null && target.equals(s)) it.remove(); } } if (incomings.isEmpty()) return null; else if (incomings.size() == 1) return incomings.get(0); else throw new IllegalArgumentException("Shape '" + s.getResourceId() + "' has more than one source: " + Arrays.toString(incomings.toArray())); } } // protected void fillInIncomings(Collection<S> shapes) { // for (S s : shapes) { // for (S o : s.getOutgoingsReadOnly()) { // if (!o.hasIncoming(s)) { // o.addIncoming(s); // } // } // } // } /** * Parse one resource to a shape object and add it to the shapes array * * @param resourceId * @param jsonShape * @param diagram * @param targetMap * @param outgoingMap * @param childMap * * @throws JSONException */ protected S parseShape(String resourceId, JSONObject jsonShape, D diagram, Map<String, String> targetMap, Map<String, List<String>> outgoingMap, Map<String, List<String>> childMap) throws JSONException { List<Point> dockers = getDockers(jsonShape, resourceId); S currentShape; if (GenericShapeImpl.isEdge(dockers)) currentShape = (S) createNewEdge(resourceId); else currentShape = (S) createNewNode(resourceId); // parse all fields currentShape.setDockers(dockers); parseStencil(jsonShape, currentShape);//, diagram.getStencilsetRef()); parseProperties(jsonShape, currentShape); // move actual association to later step parseOutgoings(jsonShape, currentShape, outgoingMap); parseChildShapes(jsonShape, currentShape, childMap); // has been moved up to give enough hints for deciding whether it should // be Node or Edge // parseDockers(modelJSON, current); parseBounds(jsonShape, currentShape); if (currentShape instanceof GenericEdge) { parseTarget(jsonShape, (GenericEdge<S, D>) currentShape, targetMap); } parseLabels(jsonShape, currentShape); //set the diagram, because will cause inconsistencies if skipped! currentShape.setDiagram(diagram); return currentShape; } protected void parseLabels(JSONObject jsonShape, S currentShape) throws JSONException { if (jsonShape.has("labels")) { List<LabelSettings> labelSettings = new ArrayList<LabelSettings>(); JSONArray labels = jsonShape.getJSONArray("labels"); for (int i = 0; i < labels.length(); i++) { JSONObject jsonLabel = labels.getJSONObject(i); LabelSettings label = new LabelSettings(); if (jsonLabel.has("x") && jsonLabel.has("y")) label.setPosition(new Point(jsonLabel.getDouble("x"), jsonLabel.getDouble("y"))); if (jsonLabel.has("distance")) label.setDistance((float) jsonLabel.getDouble("distance")); if (jsonLabel.has("ref")) label.setReference(jsonLabel.getString("ref")); if (jsonLabel.has("from")) label.setFrom(jsonLabel.getInt("from")); if (jsonLabel.has("to")) label.setTo(jsonLabel.getInt("to")); if (jsonLabel.has("align")) label.setAlignHorizontal(HorizontalAlign.fromString(jsonLabel.getString("align"))); if (jsonLabel.has("valign")) label.setAlignVertical(VerticalAlign.fromString(jsonLabel.getString("valign"))); if (jsonLabel.has("edge")) label.setEdgePos(EdgePosition.fromString(jsonLabel.getString("edge"))); if (jsonLabel.has("orientation")) label.setOrientation(LabelOrientation.fromString(jsonLabel.getString("orientation"))); Anchors anchors = new Anchors(); if (jsonLabel.has("top") && jsonLabel.getBoolean("top")) anchors.addAnchor(Anchor.TOP); if (jsonLabel.has("right") && jsonLabel.getBoolean("right")) anchors.addAnchor(Anchor.RIGHT); if (jsonLabel.has("bottom") && jsonLabel.getBoolean("bottom")) anchors.addAnchor(Anchor.BOTTOM); if (jsonLabel.has("left") && jsonLabel.getBoolean("left")) anchors.addAnchor(Anchor.LEFT); label.setAnchors(anchors); parseLabelStyles(jsonLabel, label); labelSettings.add(label); } currentShape.setLabelSettings(labelSettings); } } protected void parseLabelStyles(JSONObject jsonLabel, LabelSettings labelSetting) throws JSONException { if (jsonLabel.has("styles")) { JSONObject jsonStyles = jsonLabel.getJSONObject("styles"); // create a new style object and fill it LabelStyle style = new LabelStyle(); style.setFontFamily(jsonStyles.optString("family", null)); if (jsonStyles.has("size")) { double size = jsonStyles.optDouble("size", Double.NaN); if (size == Double.NaN) throw new IllegalArgumentException("Invalid size value: " + jsonStyles.toString()); else style.setFontSize(size); } style.setBold(jsonStyles.optBoolean("bold", false)); style.setItalic(jsonStyles.optBoolean("italic", false)); if (jsonStyles.has("fill")) { try { style.setFill(Color.decode(jsonStyles.getString("fill"))); } catch (NumberFormatException e) { throw new IllegalArgumentException("Label fill color could not be decoded: " + jsonStyles.toString(), e); } } // set the style object labelSetting.setStyle(style); } } /** * parse the stencil out of a JSONObject and set it to the current shape * * @param jsonShape * @param currentShape * @throws JSONException */ protected void parseStencil(JSONObject jsonShape, S currentShape)//, StencilSetReference stencilsetRef) throws JSONException { // get stencil id if (jsonShape.has("stencil")) { JSONObject stencil = jsonShape.getJSONObject("stencil"); String stencilString = ""; if (stencil.has("id") && !stencil.getString("id").trim().equals("")) { stencilString = stencil.getString("id"); } else { throw new IllegalArgumentException("No id found for stencil"); } currentShape.setStencilId(stencilString); } } /** * crates a StencilSet object and add it to the current diagram * * @param modelJSON * @param current * @throws JSONException */ protected void parseStencilSet(JSONObject modelJSON, D current) throws JSONException { // get stencil type if (modelJSON.has("stencilset")) { JSONObject object = modelJSON.getJSONObject("stencilset"); StencilSetReference ssRef; if (object.has("namespace") && !object.getString("namespace").trim().equals("")) ssRef = new StencilSetReference(object.getString("namespace")); else throw new IllegalArgumentException("No namespace found for stencil set"); if (object.has("url")) ssRef.setUrl(object.getString("url")); current.setStencilsetRef(ssRef); } } /** * Adds all JSON properties to the current shape. Preserves the data types of properties as found in the JSON. * * @param modelJSON * @param current * @throws JSONException */ @SuppressWarnings("unchecked") protected void parseProperties(JSONObject modelJSON, S current) throws JSONException { if (modelJSON.has("properties")) { JSONObject propsObject = modelJSON.getJSONObject("properties"); Iterator<String> keys = propsObject.keys(); while (keys.hasNext()) { String key = keys.next(); Object value = propsObject.get(key); current.setProperty(key, value); } } } /** * adds all json extension to an diagram * * @param modelJSON * @param current * @throws JSONException */ protected void parseSsextensions(JSONObject modelJSON, D current) throws JSONException { if (modelJSON.has("ssextensions")) { JSONArray array = modelJSON.getJSONArray("ssextensions"); for (int i = 0; i < array.length(); i++) { current.addSsextension(array.getString(i)); } } } /** * Parse the outgoings of a json object and update the outgoingList, to be able to add all shape references to the * current shape later. * * @param jsonShape * @param currentShape * @param outgoingMap * @throws JSONException */ protected void parseOutgoings(JSONObject jsonShape, S currentShape, Map<String, List<String>> outgoingMap) throws JSONException { if (jsonShape.has("outgoing")) { JSONArray outgoingsArray = jsonShape.getJSONArray("outgoing"); List<String> outgoingsList = new ArrayList<String>(); // outgoingMap.get(current.getResourceId()); for (int i = 0; i < outgoingsArray.length(); i++) { outgoingsList.add(outgoingsArray.getJSONObject(i).getString("resourceId")); } if (outgoingsList.size() > 0) outgoingMap.put(currentShape.getResourceId(), outgoingsList); } } /** * creates a shape list containing all child shapes and set it to the current shape new shape get added to the shape * array * * @param jsonShape * @param currentShape * @throws JSONException */ protected void parseChildShapes(JSONObject jsonShape, S currentShape, Map<String, List<String>> childMap) throws JSONException { if (jsonShape.has("childShapes")) { List<String> childShapes = new ArrayList<String>(); JSONArray childShapeObject = jsonShape.getJSONArray("childShapes"); for (int i = 0; i < childShapeObject.length(); i++) { childShapes.add(childShapeObject.getJSONObject(i).getString("resourceId")); } if (childShapes.size() > 0) { childMap.put(currentShape.getResourceId(), childShapes); } } } /** * creates a point array of all dockers and add it to the current shape * * @param modelJSON * @param shapeId * @throws JSONException */ protected List<Point> getDockers(JSONObject modelJSON, String shapeId) throws JSONException { List<Point> dockers = new ArrayList<Point>(); if (modelJSON.has("dockers")) { JSONArray dockersObject = modelJSON.getJSONArray("dockers"); for (int i = 0; i < dockersObject.length(); i++) { JSONObject docker = dockersObject.getJSONObject(i); Double x, y; if (isPropertyUndefined(docker, "x")) { LOGGER.warn("Couldn't parse docker coordinate, " + "setting to 'null' (id='" + shapeId + "'"); x = null; } else { x = dockersObject.getJSONObject(i).getDouble("x"); } if (isPropertyUndefined(docker, "y")) { LOGGER.warn("Couldn't parse docker coordinate, " + "setting to 'null' (id='" + shapeId + "'"); y = null; } else { y = dockersObject.getJSONObject(i).getDouble("y"); } if (y == null || x == null) dockers.add(null); else dockers.add(new Point(x, y)); } } return dockers; } /** * creates a bounds object with both point parsed from the json and set it to the current shape * * @param modelJSON * @param current * @throws JSONException */ protected void parseBounds(JSONObject modelJSON, S current) throws JSONException { if (modelJSON.has("bounds")) { JSONObject boundsObject = modelJSON.getJSONObject("bounds"); try { current.setBounds(new Bounds(new Point(boundsObject.getJSONObject("lowerRight").getDouble("x"), boundsObject.getJSONObject("lowerRight").getDouble("y")), new Point(boundsObject.getJSONObject( "upperLeft").getDouble("x"), boundsObject.getJSONObject("upperLeft").getDouble("y")))); } catch (JSONException e) { LOGGER.warn("Couldn't parse bounds, " + "setting to 'null' (id='" + current.getResourceId() + "'"); current.setBounds(null); } } } /** * Parse the target resource and update targetMap to be able to add it to the current shape later. * * @param jsonShape * @param currentEdge * @param targetMap * @throws JSONException */ protected void parseTarget(JSONObject jsonShape, GenericEdge<S, D> currentEdge, Map<String, String> targetMap) throws JSONException { if (jsonShape.has("target")) { JSONObject targetObject = jsonShape.getJSONObject("target"); if (targetObject.has("resourceId")) { targetMap.put(currentEdge.getResourceId(), targetObject.getString("resourceId")); } } } /** * Prepare a model JSON for analyze, resolves the hierarchical structure creates a HashMap which contains all * resourceIds as keys and for each key the JSONObject, all id are keys of this map * * @param object * @return a map; keys: all ressourceIds; values: all child JSONObjects * @throws JSONException */ protected Map<String, JSONObject> flatRessources(JSONObject object) throws JSONException { Map<String, JSONObject> result = new HashMap<String, JSONObject>(); // no cycle in hierarchies!! if (object.has("resourceId") && object.has("childShapes")) { if (result.put(object.getString("resourceId"), object) != null) { /* * if result of put is not null, then another shape with the same id exists or there is a cyclic * dependency! */ throw new JSONException("Discovered duplicate id or cyclic dependency for resourceId '" + object.getString("resourceId") + "'"); } JSONArray childShapes = object.getJSONArray("childShapes"); for (int i = 0; i < childShapes.length(); i++) { result.putAll(flatRessources(childShapes.getJSONObject(i))); } } return result; } protected boolean isPropertyUndefined(JSONObject object, String attr) { if (!object.has(attr) || object.isNull(attr)) return true; else { String attrString; try { attrString = object.getString(attr); } catch (JSONException e) { // can't happen, since we just tested for existence attrString = null; } attrString = (attrString == null) ? null : attrString.trim(); return attrString == null || attrString.equals("") || attrString.equals("null") || attrString.equals("undefined"); } } public abstract D createNewDiagram(String resourceId); public abstract E createNewEdge(String resourceId); public abstract N createNewNode(String resourceId); public S createNewShapeOfCorrectType(String resourceId, List<Point> dockers) { S shape; if (GenericShapeImpl.isEdge(dockers)) shape = (S) createNewEdge(resourceId); else shape = (S) createNewNode(resourceId); shape.setDockers(dockers); return shape; } }